package com.joshua.spring.context;

import lombok.extern.slf4j.Slf4j;
import org.springframework.core.NamedThreadLocal;
import org.springframework.util.StringUtils;

import java.util.ArrayDeque;
import java.util.Deque;

/**
 * projectName dragon-cloud
 * package com.joshua.spring.context
 * classname  DynamicDataSourceContextHolder
 * <p>
 * description 数据源切换工具类
 * </p>
 *
 * @author <a href="mailto:joshualwork@163.com">joshua_liu</a>
 * @date 2020/11/14 10:48
 */
@Slf4j
public final class DynamicDataSourceContextHolder {

    /**
     * description: 使用桟存储
     * <pre>
     *     为了支持嵌套切换，如ABC三个service都是不同的数据源
     *     其中A的某个业务要调B的方法，B的方法需要调用C的方法。一级一级调用切换，形成了链。
     *     传统的只设置当前线程的方式不能满足此业务需求，必须使用栈，后进先出。
     * </pre>
     *
     * @date 2020/11/14 10:53
     */
    private static final ThreadLocal<Deque<String>> LOOKUP_KEY_HOLDER = new NamedThreadLocal<Deque<String>>("DynamicDatasource") {
        @Override
        protected Deque<String> initialValue() {
            return new ArrayDeque<>();
        }
    };

    private DynamicDataSourceContextHolder() {
    }

    /**
     * description: 获得当前线程数据源
     * <pre>
     *    {@link Deque#peek()}: 获取第一个元素，如果返回null
     * </pre>
     *
     * @return {@link String} 数据源Key
     * @author <a href="mailto:joshualwork@163.com">joshua_liu</a>
     * @date 2020/11/14 11:03
     */
    public static String getCurrentDs() {
        return LOOKUP_KEY_HOLDER.get().peek();
    }

    /**
     * description: 设置当前线程数据源
     * <pre>
     *     调用后,一定{@link this#clear()}确保最终清除
     *     eg: try {
     *         DynamicDataSourceContextHolder.setCurrentDs(ds);
     *     } catch(Exception e) {
     *         if(isDebug()){
     *             log.debug("",e);
     *         }
     *     } finally{
     *         DynamicDataSourceContextHolder.clear();
     *     }
     * </pre>
     *
     * @param ds 数据源名称
     * @author <a href="mailto:joshualwork@163.com">joshua_liu</a>
     * @date 2020/11/14 11:05
     */
    public static void setCurrentDs(String ds) {
        if (StringUtils.isEmpty(ds)) {
            log.warn("Param datasource key is empty,not set datasource,please check you param");
        } else {
            LOOKUP_KEY_HOLDER.get().push(ds);
        }
    }

    /**
     * 移除当前数据源
     * <p>
     * 如果当前线程是连续切换数据源 只会移除掉当前的数据源
     * {@link Deque#poll()}:删除队列中第一个元素，并返回该元素的值,
     * 如果元素为null,将返回null(其实调用的是pollFirst())
     * </p>
     */
    public static void removeCurrentDs() {
        Deque<String> deque = LOOKUP_KEY_HOLDER.get();
        deque.poll();
        if (deque.isEmpty()) {
            LOOKUP_KEY_HOLDER.remove();
        }
    }

    /**
     * description: 强制清空当前线程数据源,防止内存泄漏
     * <pre>
     *     如手动调用了{@link this#setCurrentDs(String)}
     *     可调用此方法确保清除
     * </pre>
     *
     * @author <a href="mailto:joshualwork@163.com">joshua_liu</a>
     * @date 2020/11/14 11:19
     */
    public static void clear() {
        LOOKUP_KEY_HOLDER.remove();
    }
}
